﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;

namespace ZOIL.DataModel
{
    /// <summary>
    /// Singleton class, that loads plugins from dlls by type or attribute
    /// </summary>
    public class PluginManager
    {
        #region Singleton
        private static PluginManager _instance;

        public static PluginManager Instance
        {
            get { return _instance ?? (_instance = new PluginManager()); }
        }

        #endregion

        #region constructor

        private PluginManager()
        {
            _loadedAssemblies = new List<Assembly>();
            var exeName = Assembly.GetEntryAssembly().CodeBase;

            var directoryName = Path.GetDirectoryName(exeName);

            if (directoryName == null) return;

            var url = new Uri(Path.Combine(directoryName, "Plugins"));
            PluginFolder = url.AbsolutePath;

            if (!Directory.Exists(PluginFolder))
            {
                Directory.CreateDirectory(PluginFolder);
            }
            _registeredPlugins = new Dictionary<Type, List<Type>>();
            _registeredAttributes = new Dictionary<Type, List<Type>>();
        }

        #endregion

        #region public properties

        /// <summary>
        /// The path to the Plugins folder in the applications directory
        /// </summary>
        public String PluginFolder { get; private set; }

        #endregion

        private List<Assembly> _loadedAssemblies;

        #region public methods

        /// <summary>
        /// loads all *.dll from the plugin directory into the app domain
        /// </summary>
        public void LoadPlugins()
        {
            _loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();

            var files = Directory.GetFiles(PluginFolder, "*.dll");

            foreach (var file in files)
            {
                try
                {
                    _loadedAssemblies.Add(Assembly.LoadFrom(file));
                }
                catch (Exception)
                {
                    // empty
                }
            }

            foreach (var refAss in Assembly.GetEntryAssembly().GetReferencedAssemblies())
            {
                if (_loadedAssemblies.Any(a => a.FullName == refAss.FullName))
                {
                    continue;
                }
                try
                {
                    _loadedAssemblies.Add(AppDomain.CurrentDomain.Load(refAss));
                }
                catch (Exception)
                {
                    // empty
                }
            }

            var files2 = Directory.GetFiles(".", "*.dll");

            foreach (var file in files2)
            {
                try
                {
                    var refAss = Assembly.LoadFrom(file);

                    if (_loadedAssemblies.Any(a => a.FullName == refAss.FullName))
                    {
                        continue;
                    }
                    _loadedAssemblies.Add(refAss);
                }
                catch (Exception)
                {
                    // empty
                }
            }
        }

        /// <summary>
        /// scans the loaded assemblies for types that where registered with RegisterPlugin and Attributes that were registered with RegisterAttribute.
        /// </summary>
        public void Scan()
        {
            foreach (var assembly in _loadedAssemblies)
            {
                try
                {
                    var types = assembly.GetTypes();

                    foreach (var type in types)
                    {
                        //let's skip some types that are unimportant for 
                        //we need classes that are public and not abstract
                        if (!type.IsClass || type.IsNotPublic || type.IsAbstract)
                        {
                            continue;
                        }

                        //check the interfaces
                        CheckType(type);

                        //check the attributes
                        CheckAttribute(type);
                    }
                }
                catch (ReflectionTypeLoadException rtle)
                {
                    foreach (var e in rtle.LoaderExceptions)
                    {
#if DEBUG
                        Console.WriteLine(e.Message);
#endif
                    }
                }
            }
        }

        #endregion

        #region Types

        /// <summary>
        /// stores the registered plugins
        /// </summary>
        private readonly Dictionary<Type, List<Type>> _registeredPlugins;

        /// <summary>
        /// Registers a type to act as a plugin
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public void RegisterPlugin<T>()
        {
            var l = new List<Type>();
            _registeredPlugins.Add(typeof (T), l);
        }

        /// <summary>
        /// checks a given type if it contains interfaces that were registered previously#
        /// if so, the type is added to the dictionary of plugins
        /// </summary>
        /// <param name="targetType"></param>
        private void CheckType(Type targetType)
        {
            //let's check all interfaces of that class if they match any of the registered plugins
            Type[] interfaces = targetType.GetInterfaces();
            foreach (Type i in interfaces)
            {
                if (_registeredPlugins.ContainsKey(i))
                {
                    _registeredPlugins[i].Add(targetType);
                }
            }
        }

        /// <summary>
        /// returns a list of plugins containing the specified type
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public List<T> GetPlugins<T>()
        {
            if (_registeredPlugins.ContainsKey(typeof (T)))
            {
                try
                {
                    var l = new List<T>(_registeredPlugins[typeof (T)].Cast<T>());
                    return l;
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error in PluginManager: " + ex.Message);
                    throw;
                }
            }
            return new List<T>();
        }

        /// <summary>
        /// returns a list of types that contain the interface of type 'pluginType'
        /// </summary>
        /// <param name="pluginType"></param>
        /// <returns></returns>
        public List<Type> GetPluginTypes(Type pluginType)
        {
            if (_registeredPlugins.ContainsKey(pluginType))
            {
                return _registeredPlugins[pluginType];
            }
            return new List<Type>();
        }

        #endregion

        #region Attributes

        /// <summary>
        /// dictionary that holds all Types containing a specific attribute
        /// </summary>
        public Dictionary<Type, List<Type>> _registeredAttributes;

        /// <summary>
        /// Registers an attribute
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public void RegisterAttribute<T>() where T : Attribute
        {
            var l = new List<Type>();
            _registeredAttributes.Add(typeof (T), l);
        }

        /// <summary>
        /// returns all types which contain the specified attribute
        /// </summary>
        /// <param name="attributeType"></param>
        /// <returns></returns>
        public List<Type> GetAttributedTypes(Type attributeType)
        {
            return _registeredAttributes.ContainsKey(attributeType) ? _registeredAttributes[attributeType] : new List<Type>();
        }

        /// <summary>
        /// checks a given type if it contains attributes that were registered previously
        /// if so the type is added to the dictionary
        /// </summary>
        /// <param name="targetType"></param>
        private void CheckAttribute(Type targetType)
        {
            //let's check all attributes of that class, if they match any of the registered attributes
            var atts = Attribute.GetCustomAttributes(targetType);
            foreach (var att in atts)
            {
                var attType = att.GetType();
                if (_registeredAttributes.ContainsKey(attType))
                {
                    _registeredAttributes[attType].Add(targetType);
                }
            }
        }

        #endregion
    }
}